using System;
using System.Collections.Generic;
using System.Diagnostics;
using ProtoBuf;

namespace GeoAzure.Core
{
    [Serializable]
    [ProtoContract]
    public struct GeoRectangle : IEquatable<GeoRectangle>
    {
        public static readonly GeoRectangle NullRectangle = new GeoRectangle(Location.FromDegrees(0F, 0F), Location.FromDegrees(0F, 0F));

        #region Factory methods

        public static GeoRectangle Create(Location topLeft, Location bottomRight)
        {
            return new GeoRectangle(topLeft, bottomRight);
        }

        public static GeoRectangle CreateMinimumBoundingRectangle(IEnumerable<Location> locations)
        {
            GeoRectangle result;
            var enumerator = locations.GetEnumerator();
            Location location;
            if (enumerator.MoveNext())
            {
                // initialize our result rectangle
                location = enumerator.Current;
                result = Create(location, location);
            }
            else
            {
                // sequence contains no items
                return NullRectangle;
            }

            while (enumerator.MoveNext())
            {
                location = enumerator.Current;
                result = result.EnsureContainsLocation(location);
            }

            return result;
        }

        public static GeoRectangle CreateMinimumBoundingRectangle(IEnumerable<IBounded> boundedList)
        {
            double minLatitude = Location.MaxLatitude, maxLatitude = Location.MinLatitude, minLongitude = Location.MaxLongitude, maxLongitude = Location.MinLongitude;
            bool sequenceHasElements = false;
            foreach (var bounded in boundedList)
            {
                var bounds = bounded.Bounds;
                minLatitude = Math.Min(minLatitude, bounds.BottomRight.Latitude);
                maxLatitude = Math.Max(maxLatitude, bounds.TopLeft.Latitude);
                minLongitude = Math.Min(minLongitude, bounds.TopLeft.Longitude);
                maxLongitude = Math.Max(maxLongitude, bounds.BottomRight.Longitude);
                sequenceHasElements = true;
            }
            if (sequenceHasElements)
            {
                return new GeoRectangle(new Location(maxLatitude, minLongitude), new Location(minLatitude, maxLongitude));
            }
            return NullRectangle;
        }

        internal static GeoRectangle CreateMinimumBoundingRectangle<T>(IEnumerable<IndexEntry<T>> entries)
            where T : class
        {
            double minLatitude = Location.MaxLatitude, maxLatitude = Location.MinLatitude, minLongitude = Location.MaxLongitude, maxLongitude = Location.MinLongitude;
            bool sequenceHasElements = false;
            foreach (var entry in entries)
            {
                var bounds = entry.Bounds;
                minLatitude = Math.Min(minLatitude, bounds.BottomRight.Latitude);
                maxLatitude = Math.Max(maxLatitude, bounds.TopLeft.Latitude);
                minLongitude = Math.Min(minLongitude, bounds.TopLeft.Longitude);
                maxLongitude = Math.Max(maxLongitude, bounds.BottomRight.Longitude);
                sequenceHasElements = true;
            }
            if (sequenceHasElements)
            {
                return new GeoRectangle(new Location(maxLatitude, minLongitude), new Location(minLatitude, maxLongitude));
            }
            return NullRectangle;
        }

        private GeoRectangle(Location topLeft, Location bottomRight)
        {
            Debug.Assert(topLeft.Latitude >= bottomRight.Latitude);
            Debug.Assert(topLeft.Longitude <= bottomRight.Longitude);
            _topLeft = topLeft;
            _bottomRight = bottomRight;
        }

        #endregion

        #region Containment and intersection methods

        public bool Intersects(GeoRectangle other)
        {
            bool isOverlap = other.TopLeft.Longitude <= BottomRight.Longitude &&
                             other.BottomRight.Longitude >= TopLeft.Longitude &&
                             other.TopLeft.Latitude >= BottomRight.Latitude &&
                             other.BottomRight.Latitude <= TopLeft.Latitude;
            return isOverlap;
        }

        public GeoRectangle? IntersectionWith(GeoRectangle other)
        {
            if (!Intersects(other))
                return null;
            double top = Math.Min(TopLeft.Latitude, other.TopLeft.Latitude);
            double left = Math.Max(TopLeft.Longitude, other.TopLeft.Longitude);
            double bottom = Math.Max(BottomRight.Latitude, other.BottomRight.Latitude);
            double right = Math.Min(BottomRight.Longitude, other.BottomRight.Longitude);
            return new GeoRectangle(new Location(top, left), new Location(bottom, right));
        }

        public bool Contains(Location location)
        {
            if (!location.Latitude.IsBetween(BottomRight.Latitude, TopLeft.Latitude))
                return false;
            return location.Longitude.IsBetween(TopLeft.Longitude, BottomRight.Longitude);
        }

        public GeoRectangle EnsureContainsLocation(Location location)
        {
            // first check if something to do
            if (Contains(location))
                return this;

            double top = Math.Max(TopLeft.Latitude, location.Latitude);
            double left = Math.Min(TopLeft.Longitude, location.Longitude);
            double bottom = Math.Min(BottomRight.Latitude, location.Latitude);
            double right = Math.Max(BottomRight.Longitude, location.Longitude);

            return new GeoRectangle(new Location(top, left), new Location(bottom, right));
        }

        public GeoRectangle EnsureContainsRectangle(GeoRectangle other)
        {
            var top = Math.Max(other.TopLeft.Latitude, TopLeft.Latitude);
            var left = Math.Min(other.TopLeft.Longitude, TopLeft.Longitude);
            var bottom = Math.Min(other.BottomRight.Latitude, BottomRight.Latitude);
            var right = Math.Max(other.BottomRight.Longitude, BottomRight.Longitude);

            return Create(new Location(top, left), new Location(bottom, right));
        }

        public bool Contains(GeoRectangle other)
        {
            return Contains(other.TopLeft) && Contains(other.BottomRight);
        }

        #endregion

        #region Properties

        public Location TopLeft
        {
            get { return _topLeft; }
        }

        public Location TopRight
        {
            get { return new Location(TopLeft.Latitude, BottomRight.Longitude); }
        }

        public Location BottomLeft
        {
            get { return new Location(BottomRight.Latitude, TopLeft.Longitude); }
        }

        public Location BottomRight
        {
            get { return _bottomRight; }
        }

        public double Perimeter
        {
            get
            {
                double longitudeDifference = BottomRight.Longitude - TopLeft.Longitude;

                double longitudeLength = longitudeDifference * (Math.Cos(TopLeft.Latitude) + Math.Cos(BottomRight.Latitude)) * Constants.EarthRadius;
                double latitudeLength = 2 * Constants.EarthRadius * (TopLeft.Latitude - BottomRight.Latitude);

                return longitudeLength + latitudeLength;
            }
        }

        public double Area
        {
            get
            {
                // notice: sphere area = 4*PI*R^2
                // precalculated factor for area calculation
                const double longitudeSpanFactor = Constants.EarthRadius * Constants.EarthRadius;

                Debug.Assert(Width.IsBetween(0d, 2 * Math.PI));

                // see http://mathforum.org/library/drmath/view/63767.html
                //(pi/180)R^2 |sin(lat1)-sin(lat2)| |lon1-lon2|
                return longitudeSpanFactor *
                       (Math.Sin(TopLeft.Latitude) - Math.Sin(BottomRight.Latitude)) *
                       Width;
            }
        }

        public double Width
        {
            get { return BottomRight.Longitude - TopLeft.Longitude; }
        }

        public double Height
        {
            get { return TopLeft.Latitude - BottomRight.Latitude; }
        }

        public Location Center
        {
            get { return new Location((TopLeft.Latitude + BottomRight.Latitude) / 2d, (TopLeft.Longitude + BottomRight.Longitude) / 2d); }
        }

        #endregion

        #region Equality members

        public bool Equals(GeoRectangle other)
        {
            return other._topLeft.Equals(_topLeft) && other._bottomRight.Equals(_bottomRight);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj))
                return false;
            if (obj.GetType() != typeof(GeoRectangle))
                return false;
            return Equals((GeoRectangle) obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (_topLeft.GetHashCode() * 397) ^ _bottomRight.GetHashCode();
            }
        }

        public static bool operator ==(GeoRectangle left, GeoRectangle right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(GeoRectangle left, GeoRectangle right)
        {
            return !Equals(left, right);
        }

        #endregion

        #region Member variables

        [ProtoMember(1)]
        private readonly Location _topLeft;
        [ProtoMember(2)]
        private readonly Location _bottomRight;

        #endregion
    }

    public static class Utils
    {
        public static double ToRadians(double degrees)
        {
            return degrees / 180d * Math.PI;
        }
    }

    public static class Constants
    {

        // see http://en.wikipedia.org/wiki/Earth_radius
        public const double EquatorialEarthRadius = 6378137.0;
        public const double PolarEarthRadius = 6356752.3;
        public const double EarthRadius = (EquatorialEarthRadius + PolarEarthRadius) / 2.0; // in meters
        public const double NauticalMile = 1852d;
    }
}